﻿using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace Snake {
    internal class Snake : IEnumerable {
        private List<Segment> segments = new List<Segment>();
        // 能添加的身段模子
        private Transform mold;
        // 段与段之间的间隔
        private float intervalDistance = 0.05f;
        // 蛇的缩放
        private float scaleMultiple = 1.0f;

        // 蛇的增长计算
        private int addCount = 0;
        private float addDistance = 0;

        // 蛇的行走
        private Vector3 stepDirection = Vector3.right;
        private float stepSpeed = 0.016f;
        public void setStepSpeed(float speed) {
            stepSpeed = speed;
        }
        public void setStepDirection(Vector3 dir) {
            dir.z = 0;
            stepDirection = dir;
        }

        public Snake(Transform[] nodes, Transform mold, float interval) {
            this.mold = mold;
            setIntervalDistance(interval);

            if (null != nodes) {
                for (int i = 0, L = nodes.Length; i < L; ++i) {
                    if (i > 0) {
                        Vector3 prePosition = nodes[i - 1].localPosition;
                        prePosition.x -= intervalDistance;
                        prePosition.z += 0.001f;
                        nodes[i].localPosition = prePosition;
                    }
                    Segment segment = new Segment(this, nodes[i], i);
                    segments.Add(segment);
                }
            }

            foreach (Segment e in this) {
                e.setMinDistanceOfFront(intervalDistance);
            }
        }

        /// <summary>
        /// 蛇的行走更新
        /// </summary>
        public void Step() {
            getHeader().getBody().localPosition += stepDirection * stepSpeed;
            getHeader().getBody().right = Vector3.Lerp(getHeader().getBody().right, stepDirection, Time.deltaTime*10);

            MoveUpdate();
        }

        /// <summary>
        /// 蛇增长一个长度，增加到尾巴
        /// </summary>
        private void AddOneBody() {
            Transform e = GameObject.Instantiate(mold) as Transform;
            e.parent = getIndexBody(0).parent;
            int size = segments.Count - 1;
            Segment tail = getIndexSegment(size);
            tail.setIndex(size + 1);

            Segment segment = new Segment(this, e, size);
            segments.Insert(size, segment);

            Transform preBody = segment.Previous().getBody();
            segment.getBody().localPosition = preBody.localPosition + Vector3.forward * 0.001f;
            segment.getBody().localScale = preBody.localScale;
            tail.getBody().localPosition += Vector3.forward * 0.001f;
        }

        /// <summary>
        /// 长度
        /// 1. 计算长度是在newBody == intervalDistance的时候计算的；
        /// 不然需要根据实况计算
        /// </summary>
        public float GetLength() {
            return (segments.Count - 1) * intervalDistance * scaleMultiple;
        }

        /// <summary>
        /// 1. 每增长5单位增大一单位
        /// 2. 长度超过200单位后蛇只是增大
        /// </summary>
        /// <param name="addLength"></param>
        public void AddOneLength(float addLength) {
            if (addCount >= 5) {
                if (segments.Count < 200) {
                    addCount = 0;
                }
                // addCount = 0;
                float length = GetLength();
                ScaleBody((length + addLength) / length);
                return;
            }

            Segment newBody = segments[segments.Count - 2];
            if (newBody.getMinDistanceOfFront() >= intervalDistance) {
                AddOneBody();
                newBody = segments[segments.Count - 2];
                newBody.setMinDistanceOfFront(addLength / newBody.getBody().localScale.x);
                if (newBody.getMinDistanceOfFront() >= intervalDistance) {
                    addCount++;
                }
                return;
            }

            float scale = newBody.getBody().localScale.x;
            float min = (newBody.getMinDistanceOfFront() * scale + addLength) / scale;
            min += addDistance;
            addDistance = 0;

            if (min >= intervalDistance) {
                addDistance = min - intervalDistance;
                min = intervalDistance;
                addCount++;
            }
            newBody.setMinDistanceOfFront(min);
        }

        private void MoveUpdate() {
            foreach (Segment e in this) {
                e.TowardDistance();
                e.TowardFront();
            }
        }

        private void ScaleBody(float multiple) {
            scaleMultiple *= multiple;
            foreach (Transform e in new BodyInterator(this)) {
                Vector3 s = e.localScale;
                s *= multiple;
                s.z = 1;
                e.localScale = s;
            }
        }

        public Segment getHeader() {
            if (Debug.isDebugBuild) {
                if (segments.Count == 0) throw new Exception("snake not head");
            }
            return segments[0];
        }

        public Transform getIndexBody(int index) {
            return getIndexSegment(index).getBody();
        }

        public Segment getIndexSegment(int index) {
            if (Debug.isDebugBuild && (index < 0 || index >= segments.Count)) {
                throw new Exception("getIndexSegment");
            }
            return segments[index];
        }

        public IEnumerator GetEnumerator() {
            for (int i = 0, L = segments.Count; i < L; i++) {
                yield return segments[i];
            }
        }

        private class BodyInterator : IEnumerable {
            private Snake snake;
            public BodyInterator(Snake snake) {
                this.snake = snake;
            }
            public IEnumerator GetEnumerator() {
                for (int i = 0, L = snake.segments.Count; i < L; ++i) {
                    yield return snake.segments[i].getBody();
                }
            }
        }

        private void setIntervalDistance(float distance) {
            intervalDistance = distance;
        }
    }
}
